// 基于tcp的服务器端通信代码
// 在单线程或单进程的程序下，如果某一处发生了阻塞，程序就无法继续执行
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <pthread.h>
#include <strings.h>

// 信息结构体
struct SockInfo
{
    struct sockaddr_in addr;
    int fd;
};

// 结构体数组
struct SockInfo infos[512];

// 工作函数声明
void *working(void *arg);

int main()
{
    // 1、创建监听的套接字
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    if (fd == -1)
    {
        perror("socket");
        return -1;
    }

    // 2、将创建的套接字和本地的IP端口相绑定
    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(9999);       // 转换成大端字节序（建议使用5000以上的端口号，5000以下的大部分端口号可能已经被操作系统或标准协议使用了）
    saddr.sin_addr.s_addr = INADDR_ANY; // 这个0地址表示它可以绑定本地任意的一个IP地址，这就意味着在服务器端会自动地读网卡的实际IP，网卡的实际IP是什么，服务器端就绑定这个IP，这个宏的实际值为 0 = 0.0.0.0（对于0来说，大端和小端是没有区别的，因此不需要转换）
    int ret = bind(fd, (struct sockaddr *)&saddr, sizeof(saddr));

    if (ret == -1)
    {
        perror("bind");
        return -1;
    }

    // 3、设置监听
    ret = listen(fd, 128);
    if (ret == -1)
    {
        perror("listen");
        return -1;
    }

    // 初始化结构体数组
    int max = sizeof(infos) / sizeof(infos[0]);
    for (int i = 0; i < max; ++i)
    {
        bzero(&infos[i], sizeof(infos[i])); // 把结构体数组里的每一个元素初始化为0
        infos[i].fd = -1;                   // -1表示数组元素是空闲的，可用的
    }

    // 4、阻塞并等待客户端的连接
    int addrlen = sizeof(struct sockaddr_in);
    while (1)
    {
        struct SockInfo *pinfo;
        for (int i = 0; i < max; ++i)
        {
            if (infos[i].fd == -1)
            {
                pinfo = &infos[i];
                break;
            }
        }
        int cfd = accept(fd, (struct sockaddr *)&pinfo->addr, &addrlen);
        pinfo->fd = cfd;
        if (cfd == -1)
        {
            perror("accept");
            break;
        }
        // 创建子线程
        pthread_t tid;
        pthread_create(&tid, NULL, working, pinfo);
        pthread_detach(tid);
    }

    close(fd);

    return 0;
}

void *working(void *arg)
{
    struct SockInfo *pinfo = (struct SockInfo *)arg;
    // 连接建立成功，打印客户端的IP和端口信息（保存到caddr结构体里的IP和端口都是大端的，使用的时候需要先转换成小端再输出）
    char ip[32];
    printf("客户端的IP: %s, 端口: %d\n",
           inet_ntop(AF_INET, &pinfo->addr.sin_addr.s_addr, ip, sizeof(ip)),
           ntohs(pinfo->addr.sin_port));

    // 5、通信
    while (1)
    {
        // 服务器先接收数据
        char buff[1024];
        int len = recv(pinfo->fd, buff, sizeof(buff), 0);
        if (len > 0)
        {
            printf("client say: %s\n", buff);
            send(pinfo->fd, buff, len, 0);
        }
        else if (len == 0)
        {
            printf("客户端已经断开了连接...\n");
            break;
        }
        else
        {
            perror("recv");
            break;
        }
    }

    // 6、关闭文件描述符
    close(pinfo->fd);
    pinfo->fd = -1;

    return NULL;
}
